Skip to content

feat: add sidebar scroll position persistence#75

Open
ubay1 wants to merge 5 commits into
nodejs:mainfrom
ubay1:fix/persist-sidebar-scroll-position
Open

feat: add sidebar scroll position persistence#75
ubay1 wants to merge 5 commits into
nodejs:mainfrom
ubay1:fix/persist-sidebar-scroll-position

Conversation

@ubay1

@ubay1 ubay1 commented May 21, 2026

Copy link
Copy Markdown
  • Add useScroll hook with debounced scroll event handling
  • Add useScrollToElement hook to save/restore scroll position via localStorage and in-memory NavigationStateContext
  • Add NavigationStateProvider context for session-level state
  • Update Sidebar component to use useScrollToElement with a useLayoutEffect workaround (SideBar lacks forwardRef support)

#74 fix sidebar scroll position persistence

- Add `useScroll` hook with debounced scroll event handling
- Add `useScrollToElement` hook to save/restore scroll position
  via localStorage and in-memory NavigationStateContext
- Add `NavigationStateProvider` context for session-level state
- Update `Sidebar` component to use `useScrollToElement` with
  a `useLayoutEffect` workaround (SideBar lacks forwardRef support)
@ubay1 ubay1 requested a review from a team as a code owner May 21, 2026 01:13
@vercel

vercel Bot commented May 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nodejs-learn Ready Ready Preview Jul 5, 2026 2:01am

Request Review

@cursor

cursor Bot commented May 21, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Navigation UX and client-side storage only; the aside query workaround could mis-target if multiple asides exist on a page.

Overview
Adds sidebar scroll position persistence so the learn docs nav keeps its scroll offset when users move between pages or refresh.

The app layout is wrapped in a new NavigationStateProvider that holds in-memory scroll state for the session. useScroll debounces scroll events on a DOM ref; useScrollToElement restores position on mount (context first, then localStorage under navigationState:sidebar) and saves {x, y} on scroll, with safe fallbacks when storage is unavailable.

The Sidebar wires this up via useScrollToElement('sidebar', …). Because the shared SideBar UI component does not forward refs, a useLayoutEffect assigns the ref from document.querySelector('aside') so the scroll listener attaches before useScroll runs.

RemoteLoadableBanner is removed from the main layout in the same change.

Reviewed by Cursor Bugbot for commit 8e57f94. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown

👋 Codeowner Review Request

The following codeowners have been identified for the changed files:

Team reviewers: @nodejs/nodejs-website

Please review the changes when you have a chance. Thank you! 🙏

Comment thread hooks/useScrollToElement.js Outdated
Comment thread hooks/useScrollToElement.js Outdated
Comment thread providers/navigationStateProvider.jsx
Comment thread hooks/useScrollToElement.js Outdated
Comment thread hooks/useScrollToElement.js
@RushilJalal

Copy link
Copy Markdown

Kindly merge asap, this is something which is actively causing problems for me while reading docs.

@AugustinMauroy

Copy link
Copy Markdown
Member

I didn't recall correctly @nodejs/nodejs-website but don't we solve that on ui component ?

@ubay1

ubay1 commented Jul 5, 2026

Copy link
Copy Markdown
Author

Hi @AugustinMauroy,

Just to clarify, since the website currently triggers a full browser reload on page navigation instead of behaving like a Single Page Application (SPA), all component states are completely wiped out between pages.

Because of this full reload, we can't solve it purely within a UI component's lifecycle or state. We actually need to persist the scroll position across sessions using localStorage, which is why I implemented it using a custom hook to handle this side-effect.

Let me know what you think!

Signed-off-by: Ubay Dillah <33889018+ubay1@users.noreply.github.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8e57f94. Configure here.

@@ -1,13 +1,13 @@
import TableOfContents from '@node-core/ui-components/Common/TableOfContents';
import Article from '@node-core/ui-components/Containers/Article';
import RemoteLoadableBanner from '@node-core/doc-kit/src/generators/web/ui/components/AnnouncementBanner/RemoteLoadableBanner';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Announcement banner removed accidentally

Medium Severity

The layout change drops RemoteLoadableBanner and its import while the PR only describes sidebar scroll persistence. Learn pages no longer render the remote announcement banner that previously appeared site-wide.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8e57f94. Configure here.

Comment thread hooks/useScroll.js
// Clear any pending debounced calls
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debounced scroll not saved on exit

Medium Severity

When the scroll listener unmounts, cleanup clears the debounce timer without persisting the latest scrollTop. A user who scrolls the sidebar and follows a doc link within the debounce window keeps a stale position in localStorage, so restore on the next page can be wrong.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8e57f94. Configure here.

// listener, sidebarRef.current already points to the real <aside> element.
useLayoutEffect(() => {
sidebarRef.current = document.querySelector('aside');
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong aside targeted globally

Medium Severity

Scroll persistence binds to document.querySelector('aside'), the first aside in the document. The layout renders NavBar (with a mobile navigation toggler) before the doc SideBar, so scroll save/restore may attach to the navbar drawer instead of the learn sidebar.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8e57f94. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants